-
Notifications
You must be signed in to change notification settings - Fork 168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add signal commands #20876
base: main
Are you sure you want to change the base?
feat: Add signal commands #20876
Conversation
This is not yet a complete signal implementation but only the low-level core data manipulation logic.
Test Results1 166 files + 1 1 166 suites +1 1h 39m 41s ⏱️ + 4m 38s For more details on these errors, see this check. Results for commit da62c09. ± Comparison against base commit 32c4914. This pull request removes 2 and adds 124 tests. Note that renamed tests count towards both.
♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to do a fast review, except for the following bigger files, that will be reviewed in a second round:
- MutableTreeRevision
- TreeRevision
- MutableTreeRevisionTest
- TreeRevisionTest
* A signal command that doesn't apply any change but only performs a test | ||
* that will be part of determining whether a transaction passes. | ||
*/ | ||
sealed interface TestCommand extends SignalCommand { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm a bit biased, but the name TestCommand
(and further down ValueTest
, PositionTest
, and etc.) itches my mind. At first glance, it looks like a class that belongs to src/main/test
, but in reality it is used to verify a transaction. Could this be named as VerifyCommand
or something like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ConditionCommand
for the base type and then naming the specific ones e.g. ValueCondition
?
* @param listChildren | ||
* a list of child ids, or the an list if the node has no | ||
* list children |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. Though, I prefer to get rid of this almost duplicate block.
/** | ||
* Gets the insertion position that corresponds to the beginning of the | ||
* list. | ||
* | ||
* @return a list position for the beginning of the list, not | ||
* <code>null</code> | ||
*/ | ||
public static ListPosition first() { | ||
// After edge | ||
return new ListPosition(Id.ZERO, null); | ||
} | ||
|
||
/** | ||
* Gets the insertion position that corresponds to the end of the list. | ||
* | ||
* @return a list position for the end of the list, not | ||
* <code>null</code> | ||
*/ | ||
public static ListPosition last() { | ||
// Before edge | ||
return new ListPosition(null, Id.ZERO); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I easily understand this part:
The description of the after
param says: id of the node to insert immediately after...
and description of the before
param says: id of the node to insert immediately before...
What is the role of the ZERO
? Is the ZERO
an always existing root pointer that will store the head position (or the head == tail == ZERO in case of an empty list)?
If so, calling first()
returns the before first (after the ZERO) position which makes sense. But, the last()
returns before the ZERO, which I don't simply get :) Was it supposed to be Id.MAX
?
Probably, something in the implementation is being optimized by this(?), but this representation seems a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of (premature ?) optimization: ZERO is widely used also in other contexts and has therefore a custom JSON representation to be as compact as practically possible (""
).
ZERO represents both edges of the list, i.e. both head and tail. This should be safe since they can never be mixed up with each other. Furthermore, ZERO also represents the root node but that node can never have siblings.
I agree that it can be confusing. I'm just not sure if that should be addressed by additional documentation or by removing the optimization? Or maybe just introduce an EDGE
constant that refers to the same Id
instance? It would probably be dangerous to introduce HEAD
and TAIL
constants with identical values since someone might assume them to be different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, at this point, not seeing the rest of the implementation / usages makes it hard to make a practical decision about which approach to pick. Maybe I was mentioned and I forgot: is the List implementation going to be a circular linked list? In the case, it makes sense to have only one EDGE and the before / after seems to be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would first()
and last()
mean in a circular linked list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, when I was writing the previous comment, I got confused for a sec with having one EDGE constant for both head and tail (for the before first and after last positions), vs. head and tail pointing to the same location all the time (which wasn't the case). I would say, having a separate constant such as EDGE is enough for not getting confused with the ZERO thingy. Having separate HEAD
and TAIL
constants with different values might not add much value.
* @return an accepted result if the condition is <code>true</code>, a | ||
* rejected result if the condition is <code>false</code> | ||
*/ | ||
public static OperationResult test(boolean condition, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: suggesting to rename to something like verify
:
public static OperationResult test(boolean condition, | |
public static OperationResult verify(boolean condition, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or from
, of
, fromCondition
, ofCondition
.
|
||
import com.vaadin.signals.Id; | ||
|
||
public class OperationResultTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There could be more tests here to have better coverage of all the implementations, especially verifying the Accept#onlyUpdate
, and the test
behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this round I started by looking at Node
(again), TreeRevision
, and TreeRevisionTest
classes, and I found myself a bit confused over the idea of mapChildren
and listChildren
of the Node, since in the TreeRevisionTest
it distinctly uses different Ids for these two children collections, so decided to pause here for some clarifications before looking into the MutableTreeRevision
.
Also, would it make sense to use either JSpecify or Jetbrains annotations for marking the NotNull params everywhere?
* The created node will be automatically removed if the owner is | ||
* disconnected. | ||
*/ | ||
sealed interface ScopeOwnerCommand extends SignalCommand { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sealed interface ScopeOwnerCommand extends SignalCommand { | |
sealed interface ScopeOwnedCommand extends SignalCommand { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has a field for the owner but it's owned only if that field has a non-null value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Owner" also doesn't completely feel right. To me it means like a command that owns the scope. I must admit it is a hard to name concept.
Based on the JavaDoc descriptions, the key ideas behind the interface are:
- It creates a signal node.
- The node might have an owner.
- The node is automatically removed if the owner disconnects.
Fed this to GPT4o, and it suggests the following:
If the key focus is scoping the node to an owner,
ScopedSignalCommand
is a clear and concise option. If the focus is on automatic removal,OwnerBoundCommand
orEphemeralSignalCommand
could work well.Would you like a name that emphasizes ownership, lifecycle behavior, or signal creation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing to consider is: if the owner is null, why the caller code still wants to issues this type of command to create the signal? In other words, why the owner is nullable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other words, why the owner is nullable?
Would be quite redundant to have separate "insert without owner" and "insert with owner" commands and similarly also for "put if absent without owner" and "put if absent with owner".
I have in general tried to keep the number of commands down and instead opted for some branching in the logic that handles those commands. That's why there's also a KeyTest
command that can test for absence, presence or a specific value based on a parameter value. Those different variants will be expressed as separate methods in the high-level application-facing API even though they share the same underlying low-level command type.
ScopedCommand
might be an option even though it has the same problem as ScopeOwnedCommand
in not reflecting the fact that the scoping / ownership is optional.
This type is anyways mostly an internal concern that exists mainly to avoid doing two separate instanceof
checks in assertValidTree
. The type is exposed to application code only through the validator functionality.
* optional of there is no node with the given id | ||
*/ | ||
public Optional<Data> data(Id id) { | ||
Node node = nodes.get(id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the id is not supposed to be null
, why not explicitly guard against it:
Node node = nodes.get(id); | |
Node node = nodes.get(Objects.requireNonNull(id, "id cannot be null.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All paths to this method should be guarded by unit tests. Application code is not supposed to call this method.
* a list of child ids, or the an list if the node has no | ||
* list children | ||
* @param mapChildren | ||
* a sequenced map from key to child id, or an empty map if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does a sequenced map
mean here? Is it referring to the Java 21's SequencedMap
or an ordered map? If so, shouldn't we define with a more specific interface e.g. SequencedMap
or the good old LinkedHashMap
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cannot use SequencedMap
since we still support Java 17. Don't want to use LinkedHashMap
in any signature since that's an implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then what prevents the caller code from passing an unordered map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If caller code doesn't care about ordering, then it can do whatever it wants. But we should probably make sure there are tests to verify that order remains preserved once it has been implicitly or explicitly established.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are those tests going to be added to this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I will add those tests along with some other test omissions that have been pointed out in other comments.
public record Data(Id parent, Id lastUpdate, Id scopeOwner, JsonNode value, | ||
List<Id> listChildren, | ||
Map<String, Id> mapChildren) implements Node { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the implementation of TreeRevision # assertValidTree
method, and the way it had to concat the listChildren
and mapChildren
, seems like a flag to me. Enabling the caller code to provide both, can easily result in creating an invalid Data node. One should be calculated based on the other, for instance, If the mapChildren
changes to an orderedMap such as LinkedHashMap
, then the listChildren
could be just a public method that calculates based on mapChildren.values()
. Or maybe I misunderstood the purpose of having both the mapChildren
and listChildren
side by side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some child nodes are accessed by key and some by order. There's no reason why a single node couldn't have children of both types even though that's not the typical case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I'm not understanding is: should or shouldn't the listChildren
and mapChildren.values()
contain the same set of Ids?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be no overlap. Every child should be in exactly one location - either addressable by ListPosition
or by a String
key but never both at the same time.
All operations that move a child remove the child from its current location before attaching it back again, even if moving within the same parent. Any accidental overlap should trigger an error in TreeRevision.assertValidTree()
from the !visited.add(id)
check (since the concatenation doesn't do distinct()
). Should maybe add a unit test for that case as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be no overlap. Every child should be in exactly one location - either addressable by ListPosition or by a String key but never both at the same time.
To me, the it definitely worth adding this to the javadocs.
Co-authored-by: Soroosh Taefi <[email protected]>
Quality Gate passedIssues Measures |
/** | ||
* The owner id. | ||
* | ||
* @return the owner id, nor <code>null</code> if the created signal has |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @return the owner id, nor <code>null</code> if the created signal has | |
* @return the owner id, or <code>null</code> if the created signal has |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First round of review.
Will continue with the impl next and tests.
Please confirm that my following assumptions are more or less true:
Id
is needed in signals to uniquely identify a signal object within some context. Whereaskey
is for storing signals in a Map, e.g. for signals representing each field of an entity object likePerson
. The signals in list and map are usually different, because in the "list" case they are of the same rank, and in the "map" case they belong to the parent object, e.g.Person
.- the signal command is a command that is applied to one or more nodes and basically this is something that happens under the hood what an API user tries to get or set a value to signal or for example manipulates the collection of signals. Though the commands are not only scoped to the value operations, but also to modifications in hierarchy structure, access and transactions.
- scope owners are needed for security / access layer, so that each signal allows modifications only for a given owner.
- "test" commands are needed for transactions engine.
public static final Id MAX = new Id(Long.MAX_VALUE); | ||
|
||
/* | ||
* Padding refers to the trailing = characters that are only necessary when |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Padding refers to the trailing = characters that are only necessary when | |
* Padding refers to the trailing '=' characters that are only necessary when |
* id of the node to insert immediately before, nor | ||
* <code>null</code> to not define a constraint | ||
*/ | ||
public record ListPosition(Id after, Id before) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be marked with @Nullable
from JSpecify
(it should be in Flow codebase already, it was added with the CRUD repos feature)
Correct. The id is always randomly generated (or a hardcoded constant for the root node) and carries no meaning to application logic. Application logic will very rarely even have to deal with
They are all children. The only difference is whether the child is "addressed" based its position relative to other list children or addressed based on an application-provided key. In most cases, a node has either a value, list children, or map children but not multiple types of content at the same time. This corresponds to a JSON structure where But the signal data structure is more flexible than that. Another example is XML that can be expressed so that different nodes have different meaning (though XML can also be expressed as JSON with some gotchas). A signal node that is a "list child" of another node represents an element and the signal node's value is the XML tag name. A signal node that is a "map child" of another node represents an attribute - they map key is the attribute name and the node value is the attribute value. In this way, an XML element that has both child elements and attributes will have both list children and map children.
Reading values or hierarchy does not involve any commands. Commands are only for making changes (to values and/or hierarchy) and for defining conditions for transactions. And this makes me think that the
Nope. Scope owners are for automatically deleting nodes when the owner disappears (i.e. when a Hilla client is disconnected or if a Flow component is detached), even if the owner disappears in an abrupt way where it has no chance of sending cleanup commands. The main use case for this is to automatically clean up an avatar group. What would be a better name for this concept?
Confirmed. Those commands have no use on their own but only as part of a transaction that also applies some changes through "regular" commands. |
* A signal command that doesn't apply any change but only performs a test | ||
* that will be part of determining whether a transaction passes. | ||
*/ | ||
sealed interface TestCommand extends SignalCommand { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ConditionCommand
for the base type and then naming the specific ones e.g. ValueCondition
?
* @param nodeId | ||
* id of the node to check, not <code>null</code> | ||
* @param expectedLastUpdate | ||
* the expected id of the command hat last updated this node, not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is "ID of the update" if not its commandId
field value?
* @param nodeId | ||
* id of the node to check, not <code>null</code> | ||
* @param expectedLastUpdate | ||
* the expected id of the command hat last updated this node, not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* the expected id of the command hat last updated this node, not | |
* the expected id of the command that last updated this node, not |
Well, with this explanation the name makes sense. I'd think of "Lifecycle" in the name, but it's nearly the same thing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some questions and suggestions. The MutableTreeRevisionTest
would be reviewed in the next round.
public void apply(List<SignalCommand> commands) { | ||
for (SignalCommand command : commands) { | ||
apply(command, null); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has no test coverage.
private final Id commandId = Id.random(); | ||
|
||
@Test | ||
void constuctor_modifyBase_copyNotUpdated() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo:
void constuctor_modifyBase_copyNotUpdated() { | |
void constructor_modifyBase_copyNotUpdated() { |
} | ||
|
||
@Test | ||
void constuctor_modifyCopy_baseNotUpdated() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo:
void constuctor_modifyCopy_baseNotUpdated() { | |
void constructor_modifyCopy_baseNotUpdated() { |
OperationResult result; | ||
if (data(command.nodeId()).isPresent()) { | ||
TreeManipulator manipulator = new TreeManipulator(command); | ||
result = manipulator.handleCommand(command); | ||
|
||
if (manipulator.childResults != null && resultHandler != null) { | ||
manipulator.childResults.forEach(resultHandler); | ||
} | ||
} else { | ||
result = OperationResult.fail("Node not found"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a flavor I prefer, feel free to ignore and resolve:
OperationResult result; | |
if (data(command.nodeId()).isPresent()) { | |
TreeManipulator manipulator = new TreeManipulator(command); | |
result = manipulator.handleCommand(command); | |
if (manipulator.childResults != null && resultHandler != null) { | |
manipulator.childResults.forEach(resultHandler); | |
} | |
} else { | |
result = OperationResult.fail("Node not found"); | |
} | |
OperationResult result = data(command.nodeId()).map(data -> { | |
TreeManipulator manipulator = new TreeManipulator(command); | |
var opResult = manipulator.handleCommand(command); | |
if (manipulator.childResults != null && resultHandler != null) { | |
manipulator.childResults.forEach(resultHandler); | |
} | |
return opResult; | |
}).orElseGet(() -> OperationResult.fail("Node not found")); |
|
||
// Check result object | ||
Accept accept = assertAccepted(result); | ||
assertEquals("Only alias is updated", 1, accept.updates().size()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow up for jupiter api imports:
assertEquals("Only alias is updated", 1, accept.updates().size()); | |
assertEquals(1, accept.updates().size(), "Only alias is updated"); |
assertTrue("Alias was also removed", | ||
accept.updates().containsKey(alias)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow up for jupiter api imports:
assertTrue("Alias was also removed", | |
accept.updates().containsKey(alias)); | |
assertTrue(accept.updates().containsKey(alias), | |
"Alias was also removed"); |
if (result instanceof Accept accept) { | ||
accept.updates().forEach((nodeId, update) -> { | ||
Node newNode = update.newNode(); | ||
|
||
if (newNode == null) { | ||
nodes().remove(nodeId); | ||
originalInserts().remove(nodeId); | ||
} else { | ||
nodes().put(nodeId, newNode); | ||
} | ||
}); | ||
|
||
originalInserts().putAll(accept.originalInserts()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I don't see what happens to a MutableTreeRevision
instance after a command is applied to it, I still don't have a clear understanding of why originalInserts().remove(nodeId);
should happen when a command ends up removing a node from the tree.
When the scope object that owns a node no longer exists, If the node(s) owned by that owner should be removed from the tree. Then, according to the javadoc of TreeRevision # originalInserts
, some node that is no longer exist as a Data node should be added back again when the owner is back, by replaying the same command from the original insert collection.
I grasp that If there's no removal from that collection, it grows forever, and it doesn't make sense, but at the same time, without knowing how the system is going to notify the absence of an owner to a tree, I cannot digest the logic of removing from originalInserts()
every time a command results in removing a data node.
When commented out the originalInserts().remove(nodeId);
, only those tests that are related to the removal of scope owned nodes are failing, so it is a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't get why originalInserts().remove(nodeId);
is needed too.
Original inserts in accept.originalInserts()
are only added in createNode()
by PutIfAbsentCommand
or InsertCommand
, but never removed from there, so even if some inserts are removed from map of MutableTreeRevision, they are still held in map of TreeManipulator
and re-added.
Maybe they are for different nodes, but I couldn't deduct this from the codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would leak memory if we don't remove those entires when the associated node is removed. The "happy path" is that the node is indeed supposed to be removed for good and then this is the right place to do it.
The other case is when it's only a temporary removal due to cleanup when the node owner is disconnected which means that the node owner may need it again later if it gets reconnected. To deal with that, the node owner needs to create a copy of its original inserts in the logic where it handles being disconnected and reconnected so it's outside the logic that exists in this PR.
String key = parentData.mapChildren().entrySet().stream() | ||
.filter(entry -> entry.getValue().equals(id)) | ||
.map(Entry::getKey).findAny().orElse(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Prioritize findAny()
before mapping:
String key = parentData.mapChildren().entrySet().stream() | |
.filter(entry -> entry.getValue().equals(id)) | |
.map(Entry::getKey).findAny().orElse(null); | |
String key = parentData.mapChildren().entrySet().stream() | |
.filter(entry -> entry.getValue().equals(id)) | |
.findAny().map(Entry::getKey).orElse(null); |
} | ||
|
||
useData(parentId, (node, id) -> { | ||
detachedNodes.remove(resolvedChildId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should detachedNodes.remove(resolvedChildId);
for some reason be happening within the lambda that has no dependency to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no direct data relationship but useData
checks for an error condition and we shouldn't apply any changes before that check has been performed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe worth having a comment about why it is called there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second round of review.
I'm focusing now on the MutableTreeRevision.
* <ul> | ||
* <li>All nodes are attached to the root | ||
* <li>All parent-child relationships are consistent in both directions | ||
* <li>No node is attached in multiple places | ||
* <li>All aliases target an existing data node | ||
* <li>All nodes with a matching scope owner has a matching original insert | ||
* <li>All original insert entries correspond to a node with a matching | ||
* scope owner | ||
* </ul> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How nodes map and scope owner map related to each other:
- Should they always have the same set of
Id
's, i.e. maps sizes are same always ? - Can it be so that some nodes have no scope owner, i.e. no initial insert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope owner is optional so most nodes will not have a scope owner and there will thus not be any initial insert for most nodes. Furthermore, the initial insert is only needed for the tree revision instance that owns a node whereas it would be redundant for others to keep track of it.
* @param nodeId | ||
* id of the node to check, not <code>null</code> | ||
* @param expectedLastUpdate | ||
* the expected id of the command hat last updated this node, not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other words what is the difference between commandId
and expectedLastUpdate
, if the javadoc calls them both "id of the command" ?
I would guess that the commandId
is the type of the command and expectedLastUpdate
is incremental id of the command of a given type, is that correct?
* @see SignalCommand | ||
* @see TreeRevision | ||
*/ | ||
public sealed interface OperationResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name could be CommandResult
to have a better match with the SignalCommand
.
* the new node instance or null if the operation removed the | ||
* node | ||
*/ | ||
record TreeModification(Node oldNode, Node newNode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer NodeModification
name instead, but this is minor.
* @return an accepted result if the condition is <code>true</code>, a | ||
* rejected result if the condition is <code>false</code> | ||
*/ | ||
public static OperationResult test(boolean condition, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or from
, of
, fromCondition
, ofCondition
.
if (result instanceof Accept accept) { | ||
accept.updates().forEach((nodeId, update) -> { | ||
Node newNode = update.newNode(); | ||
|
||
if (newNode == null) { | ||
nodes().remove(nodeId); | ||
originalInserts().remove(nodeId); | ||
} else { | ||
nodes().put(nodeId, newNode); | ||
} | ||
}); | ||
|
||
originalInserts().putAll(accept.originalInserts()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't get why originalInserts().remove(nodeId);
is needed too.
Original inserts in accept.originalInserts()
are only added in createNode()
by PutIfAbsentCommand
or InsertCommand
, but never removed from there, so even if some inserts are removed from map of MutableTreeRevision, they are still held in map of TreeManipulator
and re-added.
Maybe they are for different nodes, but I couldn't deduct this from the codes.
This is not yet a complete signal implementation but only the low-level core data manipulation logic.